/**
 * WTF Engine
 *
 * License... etc.
 **
 * 3D Graphics Primitives & Mesh
 *
 * Provides the following:
 *	- 3D transformations and orientations;
 *	- Representation of triangles and meshes;
 *	- Classes for creating meshes of basic
 *	  geometric objects (primitives).
 *  - helper class for creating VBO arrays
 *  - Tangent Calculator
 **
 * Authors: Dan Horgan, Sean Micklethwaite, Mac Duy Hai
 **/


#include "3d.h"
#include "graphics_functions.h"
#include "3dx.h"
#include "glext.h"
#include <list>
#include <time.h>
#include "glsl.h"

using namespace WtfGraphics;


/**
 * Given angular velocity w (a rotation of |w| about w axis), this
 * returns the effective change in orientation with respect to time.
 **/
Orientation3D	Orientation3D::getDerivative(const tVector& w)
{
	Orientation3D	wSkew(0);
	wSkew[0][1] = -w[2];
	wSkew[0][2] = w[1];
	wSkew[1][0] = w[2];
	wSkew[1][2] = -w[0];
	wSkew[2][0] = -w[1];
	wSkew[2][1] = w[0];

	return wSkew * (*this);
};

/**
 * Constructs a rotation matrix representing a rotation about an arbitrary axis.
 * @param vAxis	Axis of rotation
 * @param a		Angle of rotation
 * @return	Rotation matrix
 **/
Orientation3D Orientation3D::Rotation(const tDirection& vAxis, Angle a) {
	// Need to align the axis with the z-axis.
	tDirection 		v(vAxis.normalize());
	Orientation3D	ori; // Initialized to identity matrix
	tScalar c, s, t;

	c = cos(a.inRads());
	s = sin(a.inRads());
	t = 1 - c;

	// Set rows of matrix
	ori[0] = tVector((t * v[0] * v[0]) + c,
					  (t * v[0] * v[1]) - (s * v[2]),
					   (t * v[0] * v[2]) + (s * v[1]));
	ori[1] = tVector((t * v[0] * v[1]) + (s * v[2]),
					  (t * v[1] * v[1]) + c,
					   (t * v[1] * v[2]) - (s * v[0]));
	ori[2] = tVector((t * v[0] * v[2]) - (s * v[1]),
					  (t * v[1] * v[2]) + (s * v[0]),
					   (t * v[2] * v[2]) + c);

	return ori;
};

const std::vector<Triangle> &Mesh::getTriangles() const
{
	const std::vector<Triangle> &rt = tris;
	return rt;
}




/**** VBO Array ****/

/// Creates the buffer in the memory
void VBOArray::CreateBuffer() {
	glGenBuffersARB(1, &(this->ID));
	checkGLError("Generating VBO");

	glBindBufferARB(GL_ARRAY_BUFFER_ARB, this->ID);
	checkGLError("Binding VBO (init)");

	glBufferDataARB(GL_ARRAY_BUFFER_ARB, vertices * vertexSize * sizeof(float), buf, GL_STATIC_DRAW_ARB);
	checkGLError("Setting VBO Buffer");
}

/// Binds the buffer in the graphics memory and sets up pointers for rendering
void VBOArray::Bind() {
	glEnableClientState(GL_VERTEX_ARRAY);
	glEnableClientState(GL_NORMAL_ARRAY);

	glBindBufferARB(GL_ARRAY_BUFFER_ARB, this->ID);
	
	glVertexPointer(3, GL_FLOAT, vertexSize * sizeof(float) , NULL);
	glNormalPointer(   GL_FLOAT, vertexSize * sizeof(float) , (void *) (3 * sizeof(float)));
	checkGLError("Setting VBO Pointers");

	for(int i = 0; i < this->texCoordSets; i++) {
		glActiveTextureARB(GL_TEXTURE0 + i);
		glTexCoordPointer(2, GL_FLOAT, vertexSize * sizeof(float), (void *) ((6 + 2 * i) * sizeof(float)));
	};

	checkGLError("Binding VBO");
};

void VBOArray::Release() {
	glDisableClientState(GL_VERTEX_ARRAY);
	glDisableClientState(GL_NORMAL_ARRAY);
};

void VBOArray::BindCoordSet(int i) {
	
};

// FIXME: Use Proper Attributes
void VBOArray::BindTangents(int location) {
	glColorPointer(4, GL_FLOAT, vertexSize *  sizeof(float) , (void *) (TANGENT_OFFSET(texCoordSets) * sizeof(float)));
	glVertexAttribPointerARB(location, 4, GL_FLOAT, false, vertexSize *  sizeof(float) , (void *) (TANGENT_OFFSET(texCoordSets) * sizeof(float)));
	checkGLError("Binding Tangents");
}

/**
 * Calculates per vertex tangent information. Since the mesh is made up of pure triangles,
 * all vertices of a triangle have the same tangent. Tangents are used for finding the
 * Tangent Space Matrix, which is used by shaders to perform Normal mapping.
 */
void VBOArray::CalculateTangents() {
	// temporary variables and pointers to the buffer
	int triangles = vertices / 3;
	fVector3D *normal;
	fVector3D *tangent3D; 	// tangent vector
	float	  *tangentW; 		// tangent handedness
	fVector3D **vect = new fVector3D*[3];
	fVector2D **tex = new fVector2D*[3];

	// for each triangle
	for (int t = 0; t < triangles; t++) {
		// pick data
		for (int v = 0; v < 3; v++) {
			seekVertex(t, v);
			vect[v] = (fVector3D *) &buf[pos];
			tex[v] = (fVector2D *) &buf[pos + TEXTURE_OFFSET];
		};

		// precompute values
		fVector3D v1 = *vect[1] - *vect[0];
		fVector3D v2 = *vect[2] - *vect[0];

		fVector2D t1 = *tex[1] - *tex[0];
		fVector2D t2 = *tex[2] - *tex[0];

		// calculate tangents and bitangents components
		fVector3D sdir(v1 * t2.y() - v2 * t1.y());
		fVector3D tdir(v2 * t1.x() - v1 * t2.x());

		// set tangent for each of the vertex
		for (int v = 0; v < 3; v++) {
			// point to the tangent position and get the normal
			seekVertex(t, v);
			normal 	  = (fVector3D *) &buf[pos + NORMAL_OFFSET];
			tangent3D = (fVector3D *) &buf[pos + TANGENT_OFFSET(texCoordSets)];
			tangentW  = (float *)	  &buf[pos + TANGENT_OFFSET(texCoordSets) + 3];

			// orthogonalize
			*tangent3D = (sdir - (*normal) * (normal->dot(sdir))).normalize();
			// calculate handedness
			*tangentW = (((normal->cross(sdir)).dot(tdir)) < 0.0f) ? -1.0f : 1.0f;
		}
	}
};

/// Pushes a triangle at the back and automatically calculates a face normal
void Mesh::addTriangle(Triangle t)
{
	tris.push_back(t);
	tVector v1 = t[0] - t[2];
	tVector v2 = t[1] - t[2];
	tVector n = v1.cross(v2).normalize();
	// Normals need to be facing away from centre of mesh
	if (n.dot(t[0]) < 0)
		n = -n;
	faceNorms.push_back(n);
}

/// Merges with another mesh.
void Mesh::addMesh(Mesh* m)
{
	// Slightly messy but should be safe (?)
	// TODO: Texture Coordinate sets need rewrite
	// FIXME: If this method is not used, remove it as isn't very legal.
	tris.insert(tris.begin(), m->tris.begin(), m->tris.end());
	faceNorms.insert(faceNorms.begin(), m->faceNorms.begin(), m->faceNorms.end());
	// texCoordSet.insert(tris.begin(), m.texCoordSet.begin(), m.texCoordSet.end());
}


/**
 * Builds Vertex Buffer Objects from the current state of the mesh/any supplied
 * data collections (e.g. per vertex normals or texture coordinates sets)
 */
void Mesh::CreateVBO(std::vector<Triangle>* vnormals = NULL, std::vector<TexCoordSet>* coordSets = NULL, bool calculateTangents = false) {
	// prepare array and variables
	int triangles = tris.size();
	int texs	  = (coordSets == NULL) ? 0 : coordSets->size();
	int t, v, s;
	float x, y;
	vboArray = new VBOArray(triangles * 3, texs, (calculateTangents) ? FEATURE_TANGENTS : 0);


	// load vertices & normals
	for (t = 0; t < triangles; t++) {
		for (v = 0; v < 3; v++) {
			vboArray->seekVertex(t, v);
			vboArray->putVertex(tris[t][v]);

			// use face normals by default, unless vertex normals are available
			vboArray->putNormal((vnormals == NULL) ? this->faceNorms[t] : (*vnormals)[t][v]);

			// load texture coord sets
			for (s = 0; s < texs; s++) {
				x = (*coordSets)[s][3 * t + v][0];
				y = (*coordSets)[s][3 * t + v][1];
				vboArray->putTexCoords(s, x, y);
			}
		}
	}

	// Further Calculations
	if (calculateTangents) vboArray->CalculateTangents();

	// create buffer in the gfx memory
	vboArray->CreateBuffer();
	vboCreated = true;
}

/// Perform rendering of the mesh, using VBO
void Mesh::render()
{
	// Create VBO if not already existing
	if (!vboCreated) CreateVBO();

	// Enable states
	glEnable(GL_LIGHTING);

	// Bind VBO - setup vertices and normals
	vboArray->Bind();

	// Draw from VBO
	checkGLError("before rendering");
	glDrawArrays(GL_TRIANGLES, 0, tris.size() * 3);
	
	vboArray->Release();

	// Disable states
	glDisable(GL_LIGHTING);
}


/**** Transformed Mesh ****/

TransformedMesh::TransformedMesh(const WtfGraphics::GraphicsData::tCacheRef &rBase,
								 const tVector &pos, const Orientation3D &ori)
: GraphicsData(rBase->getName()), mrBase(rBase), mvPos(pos), mOri(ori) {
};

void TransformedMesh::render() {
	WtfGraphics::SaveView();
	WtfGraphics::Translate(mvPos);
	WtfGraphics::Rotate(mOri);

	mrBase->render();

	WtfGraphics::RestoreView();
};



/**** Model ****/

Model::Model(const WtfEngine::StringPool::Entry &sName):
GraphicsData(sName) {

};

void Model::render() {
	for(std::vector<GraphicsData::tCacheRef>::iterator i = mvrMesh.begin(); i != mvrMesh.end(); i++) {
		(*i)->render();
	};
};



//
// Mesh Primitives
//

#include "3d_primitives.h"

PrimitiveModel::PrimitiveModel(const WtfEngine::StringPool::Entry& sName)
: Mesh(sName) {
};

PrimitiveModel::PrimitiveModel(const WtfEngine::StringPool::Entry& sName, const PrimitiveModel::IPrimitive::tRef& rPrim)
: Mesh(sName) {
	InsertPrimitive(rPrim, tVector(), Orientation3D());
};

void PrimitiveModel::InsertPrimitive(const PrimitiveModel::IPrimitive::tRef &rPrim, const tVector &vPos, const Orientation3D &ori) {
	Transformation3D t(vPos, ori);
	for(int i = 0; i < rPrim->getNumTris(); i++) {
		Triangle tri = rPrim->getTriangles()[i];
		for(int j = 0; j < 3; j++) {
			tri[j] = t * tri[j];
		};
		this->addTriangle(tri);
	};
};


using namespace Primitives;

Quad::Quad(tVector c1, tVector c2, tVector c3, tVector c4) : Mesh("Quad")
{
	addTriangle(Triangle (c1, c2, c4));
	addTriangle(Triangle (c2, c3, c4));
}

Oblong::Oblong(tScalar sizeX, tScalar sizeY, tScalar sizeZ) : Mesh("Oblong")
{
	// Now 'size' means size, not half-size
	sizeX /= 2; sizeY /= 2; sizeZ /=2;

	tVector c1, c2, c3, c4, c5, c6, c7, c8;
	c1 = tVector (-sizeX, -sizeY, -sizeZ);
	c2 = tVector (-sizeX, -sizeY,  sizeZ);
	c3 = tVector (-sizeX,  sizeY, -sizeZ);
	c4 = tVector (-sizeX,  sizeY,  sizeZ);
	c5 = tVector ( sizeX, -sizeY, -sizeZ);
	c6 = tVector ( sizeX, -sizeY,  sizeZ);
	c7 = tVector ( sizeX,  sizeY, -sizeZ);
	c8 = tVector ( sizeX,  sizeY,  sizeZ);

	// XXX: Quads are garbage collected objects - they cannot be allocated on
	// the stack!!
	addMesh(new Quad(c2, c1, c3, c4));
	addMesh(new Quad(c6, c5, c7, c8));
	addMesh(new Quad(c1, c2, c6, c5));
	addMesh(new Quad(c3, c4, c8, c7));
	addMesh(new Quad(c3, c1, c5, c7));
	addMesh(new Quad(c2, c4, c8, c6));

	//WtfEngine::MemoryManager::GetInstance().Reclaim();
}

Cube::Cube(tScalar size) : Mesh("Cube")
{
	addMesh(new Oblong(size, size, size));
}

// note - cylinder is uncapped. (currently)
Cylinder::Cylinder(tScalar radius, tScalar height, int sections) : Mesh("Cylinder")
{
	tVector v_old, v_new, v_height;
	int i;
	tScalar angle;
	v_height = tVector(0.0f, height, 0.0f);

	v_new = tVector(radius, -height*0.5, 0.0f);

	for( i = 1; i <= sections; ++i)
	{
		angle = ((tScalar)i/(tScalar)sections)*2*M_PI;
		v_old = v_new;
		v_new = tVector(radius*cos(angle), -height*0.5f, radius*sin(angle));
		addMesh(new Quad(v_old, v_old + v_height, v_new + v_height, v_new));
	}
}

/* Sphere Primitive
 * Increase hsections and vsections for a smoother result. */
Sphere::Sphere(tScalar radius, int hsections, int vsections)
{
	int i, j;

	tScalar h_angle, v_angle, old_s_radius, s_radius, old_s_height, s_height;

	double *xtmp = new double[hsections];
	double *ytmp = new double[hsections];


	for(j = 0; j < hsections; ++j) {
		h_angle = ((tScalar)j/(tScalar)hsections)*2*M_PI;
		xtmp[j]= cos(h_angle);
		ytmp[j] = sin(h_angle);
	}

	s_height = -radius;
	s_radius = 0.0;

	for(i = 0; i <= vsections; ++i) {
		old_s_height = s_height;
		old_s_radius = s_radius;

		v_angle = ((tScalar)i/(tScalar)vsections)*M_PI;
		s_radius = radius*sin(v_angle);
		s_height = radius*cos(v_angle);

		for (j = 0; j < hsections; ++j) {
			int k = (j+1) % hsections;
			tVector v1, v2, v3, v4;
			v1 = tVector(xtmp[j]*old_s_radius, old_s_height, ytmp[j]*old_s_radius);
			v2 = tVector(xtmp[k]*old_s_radius, old_s_height, ytmp[k]*old_s_radius);
			v3 = tVector(xtmp[k]*s_radius, s_height, ytmp[k]*s_radius);
			v4 = tVector(xtmp[j]*s_radius, s_height, ytmp[j]*s_radius);
			addMesh(new Quad(v1, v2, v3, v4));
			// TODO setting normals
		}
	}

	delete [] xtmp;
	delete [] ytmp;
}

